Part 1: International

# Data analysis
import geopandas as gpd
import numpy as np
import pandas as pd
import intake
import dask
from shapely.geometry import Point

# APIs and data
import requests
import cenpy
import pygris

# Plotting
import seaborn as sns
from matplotlib import pyplot as plt
import altair as alt
import holoviews as hv
import hvplot.pandas
import geoviews as gv
import geoviews.tile_sources as gvts
import datashader as ds
import datashader.transfer_functions as tf
from datashader.colors import Greys9, viridis, inferno
from colorcet import fire, kgy, CET_CBL3

# Sci-Kit Learn
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
from sklearn.cluster import KMeans

pd.options.display.max_columns = 999
C:\Users\fatbo\mambaforge\envs\musa-550-fall-2023\lib\site-packages\libpysal\cg\alpha_shapes.py:39: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
  def nb_dist(x, y):
C:\Users\fatbo\mambaforge\envs\musa-550-fall-2023\lib\site-packages\libpysal\cg\alpha_shapes.py:165: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
  def get_faces(triangle):
C:\Users\fatbo\mambaforge\envs\musa-550-fall-2023\lib\site-packages\libpysal\cg\alpha_shapes.py:199: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
  def build_faces(faces, triangles_is, num_triangles, num_faces_single):
C:\Users\fatbo\mambaforge\envs\musa-550-fall-2023\lib\site-packages\libpysal\cg\alpha_shapes.py:261: NumbaDeprecationWarning: The 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.
  def nb_mask_faces(mask, faces):

Mapping Global Cell Towers

First and foremost, let’s take a broad look at cell tower coverage across the world. The OpenCellID database has a dataframe with over 200 million observations of cell towers, which we can aggregate and plot using Intake, Dask, and Datashader. Based on this visualization, we’ll notice regions with high concentrations of cell towers are primarily the USA, most of Europe, Japan, South Korea, while noticeable blind spots take place in central Africa, South America, west Asia, and rural China.

ctw = intake.open_csv('./data/cell_towers.csv')
ddf = ctw.to_dask()
FileNotFoundError: An error occurred while calling the read_csv method registered to the pandas backend.
Original Message: [WinError 2] The system cannot find the file specified: 'C:/Users/fatbo/OneDrive/Documents/GitHub/final-project-stephanie_jarred_alec/data/cell_towers.csv'
max_lon = ddf['lon'].max().compute()
min_lon = ddf['lon'].min().compute()
max_lat = ddf['lat'].max().compute()
min_lat = ddf['lat'].min().compute()

print(f"Max Longitude: {max_lon}, Min Longitude: {min_lon}")
print(f"Max Latitude: {max_lat}, Min Latitude: {min_lat}")
Max Longitude: 179.9732208252, Min Longitude: -179.9732208252
Max Latitude: 78.334579467773, Min Latitude: -54.936431
global_x_range = (-179.9732208252, 179.9732208252)
global_y_range = (-54.936431, 78.334579467773)

# Default width and height
global_plot_width = 900
global_plot_height = int(global_plot_width*0.5)
canvas = ds.Canvas(
    plot_width=global_plot_width,
    plot_height=global_plot_height,
    x_range=global_x_range,
    y_range=global_y_range,
)
agg = canvas.points(ddf, "lon", "lat", agg=ds.count())
selected = agg.where(agg > 15)
tf.set_background(tf.shade(selected, cmap=CET_CBL3),"black")

Next, we can take a closer look at the differences across global cell networks based on the radio system type. This can help us understand the difference in advancement of ICT across different countries.

We may be more familiar with these radio systems as supporting “2G,” “3G,” or “4G” mobile network. According to this article by Wilson Amplifiers:

  • GSM is the standard radio system supporting 2G network
  • CDMA is a competitor to GSM also supporting 2G network
  • UMTS of the GSM network supports 3G network
  • LTE migrates CDMA/CDMA2000 and GSM/UMTS into the 4G network
  • NR is a “New Radio” technology supporting 5G network
ddf['radio'] = ddf['radio'].astype('category')
color_key = {"GSM": "#707390",
             "CDMA": "#93b7be",
             "UMTS": "#e0ca3c",
             "LTE": "#b59ab8",
             "NR": "#048a81"}
agg = canvas.points(ddf, "lon", "lat", agg=ds.count_cat("radio"))
selected = agg.where(agg > 15)
tf.set_background(tf.shade(selected, color_key=color_key),"black")
legend_df = pd.DataFrame(list(color_key.items()), columns=["radio", "color"])


legend = (alt.Chart(legend_df)
    .mark_rect(stroke="black", strokeWidth=1)
    .encode(
        y=alt.Y("radio:N", axis=alt.Axis(title=""), sort=alt.SortField("index")),
        color=alt.Color("color:N", legend=None, scale=None),
      #  tooltip=["radio", "color"]
    )
    .properties(width=100, height=120, title="Radio Systems Guide"))

legend

With this second visualization, we’ll notice that a vast majority of cell towers across the world use UMTS radio system and support 3G mobile data, while some concentrations of LTE exist across the USA, India, East Asia, and Europe.

Global Cell Tower Coverage Dominant Types

Given these differences in cell tower types, let’s take a look at how these differences line up across other country metrics, particularly population and GDP per capita. This will allow us to conduct an analysis of cell tower coverage and type across developing and developed countries. Along with OpenCellID data, this section will use data from the World Bank.

For each country, a ‘dominant cell type’ will be determined by which type of cell tower is most common in the respective country. Unfortunately, there is no NR (new radio, 5G) data for this section.

gdp_per_capita = pd.read_csv("./data/gdp_per_capita.csv") #data from world bank
gdp_per_capita = gdp_per_capita[['Country Name', '2021 [YR2021]']]
gdp_per_capita = gdp_per_capita.rename(columns={
    'Country Name': 'country',
    '2021 [YR2021]': 'gdp_per_capita'
})
gdp_per_capita = gdp_per_capita.dropna(subset=['country'])
population = pd.read_csv("./data/population.csv") #data from world bank
population = population[['Country Name', '2021 [YR2021]']]
population = population.rename(columns={
    'Country Name': 'country',
    '2021 [YR2021]': 'population'
})
population = population.dropna(subset=['country'])
gpc_pop = population.merge(gdp_per_capita, how='left', on='country')
countries = gpd.read_file("https://datahub.io/core/geo-countries/r/countries.geojson")
countries = countries[['ADMIN', 'geometry']]
countries = countries.rename(columns={'ADMIN': 'country'})
DEBUG:fiona._env:GDAL_DATA found in environment.
DEBUG:fiona._env:PROJ_DATA found in environment.
DEBUG:fiona._env:GDAL_DATA found in environment.
DEBUG:fiona._env:PROJ_DATA found in environment.
DEBUG:fiona.ogrext:Got coordinate system
DEBUG:fiona.ogrext:Got coordinate system
DEBUG:fiona.crs:Matched. confidence=100, c_code=b'4326', c_name=b'EPSG'
DEBUG:fiona.ogrext:OLC_FASTSETNEXTBYINDEX: 1
DEBUG:fiona.ogrext:OLC_FASTFEATURECOUNT: 1
DEBUG:fiona.ogrext:Next index: 0
DEBUG:fiona.ogrext:Next index: 1
DEBUG:fiona.ogrext:Next index: 2
DEBUG:fiona.ogrext:Next index: 3
DEBUG:fiona.ogrext:Next index: 4
DEBUG:fiona.ogrext:Next index: 5
DEBUG:fiona.ogrext:Next index: 6
DEBUG:fiona.ogrext:Next index: 7
DEBUG:fiona.ogrext:Next index: 8
DEBUG:fiona.ogrext:Next index: 9
DEBUG:fiona.ogrext:Next index: 10
DEBUG:fiona.ogrext:Next index: 11
DEBUG:fiona.ogrext:Next index: 12
DEBUG:fiona.ogrext:Next index: 13
DEBUG:fiona.ogrext:Next index: 14
DEBUG:fiona.ogrext:Next index: 15
DEBUG:fiona.ogrext:Next index: 16
DEBUG:fiona.ogrext:Next index: 17
DEBUG:fiona.ogrext:Next index: 18
DEBUG:fiona.ogrext:Next index: 19
DEBUG:fiona.ogrext:Next index: 20
DEBUG:fiona.ogrext:Next index: 21
DEBUG:fiona.ogrext:Next index: 22
DEBUG:fiona.ogrext:Next index: 23
DEBUG:fiona.ogrext:Next index: 24
DEBUG:fiona.ogrext:Next index: 25
DEBUG:fiona.ogrext:Next index: 26
DEBUG:fiona.ogrext:Next index: 27
DEBUG:fiona.ogrext:Next index: 28
DEBUG:fiona.ogrext:Next index: 29
DEBUG:fiona.ogrext:Next index: 30
DEBUG:fiona.ogrext:Next index: 31
DEBUG:fiona.ogrext:Next index: 32
DEBUG:fiona.ogrext:Next index: 33
DEBUG:fiona.ogrext:Next index: 34
DEBUG:fiona.ogrext:Next index: 35
DEBUG:fiona.ogrext:Next index: 36
DEBUG:fiona.ogrext:Next index: 37
DEBUG:fiona.ogrext:Next index: 38
DEBUG:fiona.ogrext:Next index: 39
DEBUG:fiona.ogrext:Next index: 40
DEBUG:fiona.ogrext:Next index: 41
DEBUG:fiona.ogrext:Next index: 42
DEBUG:fiona.ogrext:Next index: 43
DEBUG:fiona.ogrext:Next index: 44
DEBUG:fiona.ogrext:Next index: 45
DEBUG:fiona.ogrext:Next index: 46
DEBUG:fiona.ogrext:Next index: 47
DEBUG:fiona.ogrext:Next index: 48
DEBUG:fiona.ogrext:Next index: 49
DEBUG:fiona.ogrext:Next index: 50
DEBUG:fiona.ogrext:Next index: 51
DEBUG:fiona.ogrext:Next index: 52
DEBUG:fiona.ogrext:Next index: 53
DEBUG:fiona.ogrext:Next index: 54
DEBUG:fiona.ogrext:Next index: 55
DEBUG:fiona.ogrext:Next index: 56
DEBUG:fiona.ogrext:Next index: 57
DEBUG:fiona.ogrext:Next index: 58
DEBUG:fiona.ogrext:Next index: 59
DEBUG:fiona.ogrext:Next index: 60
DEBUG:fiona.ogrext:Next index: 61
DEBUG:fiona.ogrext:Next index: 62
DEBUG:fiona.ogrext:Next index: 63
DEBUG:fiona.ogrext:Next index: 64
DEBUG:fiona.ogrext:Next index: 65
DEBUG:fiona.ogrext:Next index: 66
DEBUG:fiona.ogrext:Next index: 67
DEBUG:fiona.ogrext:Next index: 68
DEBUG:fiona.ogrext:Next index: 69
DEBUG:fiona.ogrext:Next index: 70
DEBUG:fiona.ogrext:Next index: 71
DEBUG:fiona.ogrext:Next index: 72
DEBUG:fiona.ogrext:Next index: 73
DEBUG:fiona.ogrext:Next index: 74
DEBUG:fiona.ogrext:Next index: 75
DEBUG:fiona.ogrext:Next index: 76
DEBUG:fiona.ogrext:Next index: 77
DEBUG:fiona.ogrext:Next index: 78
DEBUG:fiona.ogrext:Next index: 79
DEBUG:fiona.ogrext:Next index: 80
DEBUG:fiona.ogrext:Next index: 81
DEBUG:fiona.ogrext:Next index: 82
DEBUG:fiona.ogrext:Next index: 83
DEBUG:fiona.ogrext:Next index: 84
DEBUG:fiona.ogrext:Next index: 85
DEBUG:fiona.ogrext:Next index: 86
DEBUG:fiona.ogrext:Next index: 87
DEBUG:fiona.ogrext:Next index: 88
DEBUG:fiona.ogrext:Next index: 89
DEBUG:fiona.ogrext:Next index: 90
DEBUG:fiona.ogrext:Next index: 91
DEBUG:fiona.ogrext:Next index: 92
DEBUG:fiona.ogrext:Next index: 93
DEBUG:fiona.ogrext:Next index: 94
DEBUG:fiona.ogrext:Next index: 95
DEBUG:fiona.ogrext:Next index: 96
DEBUG:fiona.ogrext:Next index: 97
DEBUG:fiona.ogrext:Next index: 98
DEBUG:fiona.ogrext:Next index: 99
DEBUG:fiona.ogrext:Next index: 100
DEBUG:fiona.ogrext:Next index: 101
DEBUG:fiona.ogrext:Next index: 102
DEBUG:fiona.ogrext:Next index: 103
DEBUG:fiona.ogrext:Next index: 104
DEBUG:fiona.ogrext:Next index: 105
DEBUG:fiona.ogrext:Next index: 106
DEBUG:fiona.ogrext:Next index: 107
DEBUG:fiona.ogrext:Next index: 108
DEBUG:fiona.ogrext:Next index: 109
DEBUG:fiona.ogrext:Next index: 110
DEBUG:fiona.ogrext:Next index: 111
DEBUG:fiona.ogrext:Next index: 112
DEBUG:fiona.ogrext:Next index: 113
DEBUG:fiona.ogrext:Next index: 114
DEBUG:fiona.ogrext:Next index: 115
DEBUG:fiona.ogrext:Next index: 116
DEBUG:fiona.ogrext:Next index: 117
DEBUG:fiona.ogrext:Next index: 118
DEBUG:fiona.ogrext:Next index: 119
DEBUG:fiona.ogrext:Next index: 120
DEBUG:fiona.ogrext:Next index: 121
DEBUG:fiona.ogrext:Next index: 122
DEBUG:fiona.ogrext:Next index: 123
DEBUG:fiona.ogrext:Next index: 124
DEBUG:fiona.ogrext:Next index: 125
DEBUG:fiona.ogrext:Next index: 126
DEBUG:fiona.ogrext:Next index: 127
DEBUG:fiona.ogrext:Next index: 128
DEBUG:fiona.ogrext:Next index: 129
DEBUG:fiona.ogrext:Next index: 130
DEBUG:fiona.ogrext:Next index: 131
DEBUG:fiona.ogrext:Next index: 132
DEBUG:fiona.ogrext:Next index: 133
DEBUG:fiona.ogrext:Next index: 134
DEBUG:fiona.ogrext:Next index: 135
DEBUG:fiona.ogrext:Next index: 136
DEBUG:fiona.ogrext:Next index: 137
DEBUG:fiona.ogrext:Next index: 138
DEBUG:fiona.ogrext:Next index: 139
DEBUG:fiona.ogrext:Next index: 140
DEBUG:fiona.ogrext:Next index: 141
DEBUG:fiona.ogrext:Next index: 142
DEBUG:fiona.ogrext:Next index: 143
DEBUG:fiona.ogrext:Next index: 144
DEBUG:fiona.ogrext:Next index: 145
DEBUG:fiona.ogrext:Next index: 146
DEBUG:fiona.ogrext:Next index: 147
DEBUG:fiona.ogrext:Next index: 148
DEBUG:fiona.ogrext:Next index: 149
DEBUG:fiona.ogrext:Next index: 150
DEBUG:fiona.ogrext:Next index: 151
DEBUG:fiona.ogrext:Next index: 152
DEBUG:fiona.ogrext:Next index: 153
DEBUG:fiona.ogrext:Next index: 154
DEBUG:fiona.ogrext:Next index: 155
DEBUG:fiona.ogrext:Next index: 156
DEBUG:fiona.ogrext:Next index: 157
DEBUG:fiona.ogrext:Next index: 158
DEBUG:fiona.ogrext:Next index: 159
DEBUG:fiona.ogrext:Next index: 160
DEBUG:fiona.ogrext:Next index: 161
DEBUG:fiona.ogrext:Next index: 162
DEBUG:fiona.ogrext:Next index: 163
DEBUG:fiona.ogrext:Next index: 164
DEBUG:fiona.ogrext:Next index: 165
DEBUG:fiona.ogrext:Next index: 166
DEBUG:fiona.ogrext:Next index: 167
DEBUG:fiona.ogrext:Next index: 168
DEBUG:fiona.ogrext:Next index: 169
DEBUG:fiona.ogrext:Next index: 170
DEBUG:fiona.ogrext:Next index: 171
DEBUG:fiona.ogrext:Next index: 172
DEBUG:fiona.ogrext:Next index: 173
DEBUG:fiona.ogrext:Next index: 174
DEBUG:fiona.ogrext:Next index: 175
DEBUG:fiona.ogrext:Next index: 176
DEBUG:fiona.ogrext:Next index: 177
DEBUG:fiona.ogrext:Next index: 178
DEBUG:fiona.ogrext:Next index: 179
DEBUG:fiona.ogrext:Next index: 180
DEBUG:fiona.ogrext:Next index: 181
DEBUG:fiona.ogrext:Next index: 182
DEBUG:fiona.ogrext:Next index: 183
DEBUG:fiona.ogrext:Next index: 184
DEBUG:fiona.ogrext:Next index: 185
DEBUG:fiona.ogrext:Next index: 186
DEBUG:fiona.ogrext:Next index: 187
DEBUG:fiona.ogrext:Next index: 188
DEBUG:fiona.ogrext:Next index: 189
DEBUG:fiona.ogrext:Next index: 190
DEBUG:fiona.ogrext:Next index: 191
DEBUG:fiona.ogrext:Next index: 192
DEBUG:fiona.ogrext:Next index: 193
DEBUG:fiona.ogrext:Next index: 194
DEBUG:fiona.ogrext:Next index: 195
DEBUG:fiona.ogrext:Next index: 196
DEBUG:fiona.ogrext:Next index: 197
DEBUG:fiona.ogrext:Next index: 198
DEBUG:fiona.ogrext:Next index: 199
DEBUG:fiona.ogrext:Next index: 200
DEBUG:fiona.ogrext:Next index: 201
DEBUG:fiona.ogrext:Next index: 202
DEBUG:fiona.ogrext:Next index: 203
DEBUG:fiona.ogrext:Next index: 204
DEBUG:fiona.ogrext:Next index: 205
DEBUG:fiona.ogrext:Next index: 206
DEBUG:fiona.ogrext:Next index: 207
DEBUG:fiona.ogrext:Next index: 208
DEBUG:fiona.ogrext:Next index: 209
DEBUG:fiona.ogrext:Next index: 210
DEBUG:fiona.ogrext:Next index: 211
DEBUG:fiona.ogrext:Next index: 212
DEBUG:fiona.ogrext:Next index: 213
DEBUG:fiona.ogrext:Next index: 214
DEBUG:fiona.ogrext:Next index: 215
DEBUG:fiona.ogrext:Next index: 216
DEBUG:fiona.ogrext:Next index: 217
DEBUG:fiona.ogrext:Next index: 218
DEBUG:fiona.ogrext:Next index: 219
DEBUG:fiona.ogrext:Next index: 220
DEBUG:fiona.ogrext:Next index: 221
DEBUG:fiona.ogrext:Next index: 222
DEBUG:fiona.ogrext:Next index: 223
DEBUG:fiona.ogrext:Next index: 224
DEBUG:fiona.ogrext:Next index: 225
DEBUG:fiona.ogrext:Next index: 226
DEBUG:fiona.ogrext:Next index: 227
DEBUG:fiona.ogrext:Next index: 228
DEBUG:fiona.ogrext:Next index: 229
DEBUG:fiona.ogrext:Next index: 230
DEBUG:fiona.ogrext:Next index: 231
DEBUG:fiona.ogrext:Next index: 232
DEBUG:fiona.ogrext:Next index: 233
DEBUG:fiona.ogrext:Next index: 234
DEBUG:fiona.ogrext:Next index: 235
DEBUG:fiona.ogrext:Next index: 236
DEBUG:fiona.ogrext:Next index: 237
DEBUG:fiona.ogrext:Next index: 238
DEBUG:fiona.ogrext:Next index: 239
DEBUG:fiona.ogrext:Next index: 240
DEBUG:fiona.ogrext:Next index: 241
DEBUG:fiona.ogrext:Next index: 242
DEBUG:fiona.ogrext:Next index: 243
DEBUG:fiona.ogrext:Next index: 244
DEBUG:fiona.ogrext:Next index: 245
DEBUG:fiona.ogrext:Next index: 246
DEBUG:fiona.ogrext:Next index: 247
DEBUG:fiona.ogrext:Next index: 248
DEBUG:fiona.ogrext:Next index: 249
DEBUG:fiona.ogrext:Next index: 250
DEBUG:fiona.ogrext:Next index: 251
DEBUG:fiona.ogrext:Next index: 252
DEBUG:fiona.ogrext:Next index: 253
DEBUG:fiona.ogrext:Next index: 254
DEBUG:fiona.ogrext:Next index: 255
DEBUG:fiona.collection:Flushed buffer
DEBUG:fiona.collection:Stopped session
gpc_pop_countries = countries.merge(gpc_pop, how='left', on='country')
countries_towers = pd.read_csv("./data/international_cell_towers.csv")
merge = countries_towers.merge(gpc_pop_countries, how='left', on='country')
merge.rename(columns={'UMES': 'UMTS'}, inplace=True)
tower_types = ['UMTS', 'GSM', 'LTE', 'CDMA']
merge['dominant_tower_type'] = merge[tower_types].idxmax(axis=1)
merge['cell_towers'] = pd.to_numeric(merge['cell_towers'], errors='coerce')
merge['population'] = pd.to_numeric(merge['population'], errors='coerce')
merge['gdp_per_capita'] = pd.to_numeric(merge['gdp_per_capita'], errors='coerce')
merge['ctw_per_capita'] = merge['cell_towers'] / merge['population']
merge = gpd.GeoDataFrame(merge, geometry='geometry')
merge = merge.dropna(subset=['geometry'])
merge = merge.to_crs('EPSG:3857')

In the following map, we can identify quickly what the leading cell tower type is in each country, suggesting how advanced their cell tower technology is. Most notably, China, USA, Norway, Australia and Japan are leading with 4G LTE technology. Unfortunately, not all countries have sufficient data and some have been dropped from this analysis.

%%opts WMTS [width=global_plot_width, height=global_plot_height, xaxis=None, yaxis=None]

choro = merge.hvplot(
    c="dominant_tower_type",
    frame_width=600,
    frame_height=600,
    alpha=0.5,
    geo=True,
    crs='EPSG:3857',
    cmap="kgy",
    hover_cols=["country"],
    geometry='geometry',
)

gvts.EsriImagery * choro
WARNING:param.main: geometry option not found for polygons plot; similar options include: []
WARNING:param.main:geometry option not found for polygons plot; similar options include: []
merge_df = pd.DataFrame(merge)
merge_df.drop(columns=['geometry'], inplace=True)
merge_df = merge_df.dropna()

In this interactive chart, we can analyze different countries’ advancement in cell tower technology with respect to their population, GDP per capita, and cell tower per capita. Interact with the chart by selecting sections of the scatterplot to see counts of countries’ dominant tower types (only GSM, LTE, UMTS), or click on each of the dominant tower types at the barplot to see only countries of that tower type. The size of the countries’ populations are also represented through the size of the points, which you can hover over. Note that GDP per capita and cell towers per capita on the scatterplot have been logged.

brush = alt.selection_interval(encodings=['x'])
click = alt.selection_point(encodings=['color'])

points = alt.Chart(merge_df).mark_circle().encode(
    alt.X("gdp_per_capita:Q", scale=alt.Scale(zero=False, type="log"), title="Log GDP per Capita"),
    alt.Y("ctw_per_capita:Q", scale=alt.Scale(type="log"), title="Log Cell Towers per Capita"),
    size=alt.Size("population:Q", title="Population"),
    color=alt.condition(brush, alt.Color("dominant_tower_type:N", title="Dominant Tower Type", scale=alt.Scale(domain=list(color_key.keys()), range=list(color_key.values())),), alt.value('grey')),
    tooltip="country",
).properties(width=500, height=350).add_selection(brush).transform_filter(
    click
)

bars = alt.Chart(merge_df).mark_bar().encode(
    x='count()',
    y=alt.Y("dominant_tower_type:N", title="Dominant Tower Type"),
    color=alt.condition(brush, alt.Color("dominant_tower_type:N", title="Dominant Tower Type"), alt.value('lightgray')),
).transform_filter(brush).properties(width=550).add_params(
    click
)

# Combine charts
alt.vconcat(points, bars, title="World Cell Tower Coverage")

As we can see from the charts, there is not only a trend in log cell towers per capita to GDP per capita, but also a trend in less developed countries (based on the GDP per capita metric) in having less developed dominant tower type (GSM, 2G) compared to more developed countries which are more likely to have UMTS (3G) or LTE (4G). Some outliers include China, whose lower cell tower per capita may be attribued to the large proportion of rural space, The Netherlands who also have a particularly low cell tower per capita, and Belgium and Germany whose dominant tower type is GSM despite high coverage and GDP per capita.

A Clustering Analysis of Global Cell Tower Coverage

Let’s take a deeper dive into our previous graph through a clustering analysis.

scaler = StandardScaler()

merge_df_scaled = scaler.fit_transform(merge_df[['ctw_per_capita',
                                                 'gdp_per_capita',
                                                 'population']])
merge_df_scaled.mean(axis=0)
merge_df_scaled.std(axis=0)
array([1., 1., 1.])
kmeans = KMeans(n_clusters=5, n_init=10)

kmeans.fit(merge_df_scaled[:, [0, 1, 2]])
merge_df['label'] = kmeans.labels_
merge_df.groupby('label').size()
label
0     37
1      2
2      4
3    111
4     11
dtype: int64
brush = alt.selection_interval(encodings=['x'])
click = alt.selection_point(encodings=['color'])

points = alt.Chart(merge_df).mark_circle().encode(
    alt.X("gdp_per_capita:Q", scale=alt.Scale(zero=False, type="log"), title="Log GDP per Capita"),
    alt.Y("ctw_per_capita:Q", scale=alt.Scale(type="log"), title="Log Cell Towers per Capita"),
    size=alt.Size("population:Q", title="Population"),
    color=alt.Color("label:N", scale=alt.Scale(scheme="dark2")),
    tooltip="country",
).properties(width=500, height=350).add_selection(brush).transform_filter(
    click
)

bars = alt.Chart(merge_df).mark_bar().encode(
    x='count()',
    y=alt.Y("label:N", title="Cluster"),
    color=alt.condition(brush, alt.Color("label:N", title="Cluster"), alt.value('lightgray')),
).transform_filter(brush).properties(width=550).add_params(
    click
)

# Combine charts
alt.vconcat(points, bars, title="World Cell Tower Coverage in Clusters")

These clusters were created based on a range of scaled values, including cell tower per capita, GDP per capita, and population. With this, 5 clusters were created. Cluster 0 consistered of the moderately developed and decent cell tower per person coverage; cluster 1 consisted mostly of the extreme high population less developed countries India and China, cluster 2 consisted of the highly developed and well connected countries with very low population, cluster 3 consisted of the majority of developing countries with more limited log cell towers per capita, and the last cluster cluster 4 consisted of slightly richer and higher cell tower per capita countries than cluster 0.

Sources